"""
The goal of tableproc is to allow people to easily process tabular data in a 
generic manner, using simple and natural operations.

@author: Christophe Alexandre <optimacom.sarl at gmail dot com>

@license:
Copyright(C) 2010 Optimacom sarl

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/lgpl.txt>.
"""

import os

import agg

def numbered(a_list):
  """
  Utility function numbering list elements, resulting
  in a list of (number, element) pairs.
  
  @param a_list: list to be numbered
  @type a_list: list
  @rtype: list of pairs
  """
  return zip(range(len(a_list)), a_list)

class Table(object):
  """
  Represents a generic table.
  
  Defines some operations like grouping, sorting,
  computing aggregates (sum, avg, custom functions ...)
  
  @todo: setting aggregates independently from grouping
  
  """
  def __init__(self, *user_column_names):
    """
    @param user_column_names: list of unique names identifying columns
    """
    self.column_names = dict()
    self.column_names_positions = dict()
    for name, i in zip(user_column_names, range(len(user_column_names))):
      self.column_names[i] = name
      self.column_names_positions[name] = i
    self._subtables = dict()
    self._parent_table = None
    self._aggregates = dict()
    self._aggregators = dict()
    self._sorting_positions = []
    self._sorting_params = dict()
    self.level = 1
    
  def _create_subtable(self):
    t = Table()
    t._parent_table = self
    t.set_level(self.level + 1)
    return t
  
  def _attach_table(self, t, m):
    self._subtables[m] = t
    t._parent_table = self
    t.set_level(self.level + 1)
    
  def sort(self, *column_names, **params):
    """
    Sorting based on specified column names.
    
    @todo: custom comparators.
    
    @param column_names: column names for ordering
    @param params: extra options
    
    @keyword reverse: boolean reversing ordering if True.
    @keyword deep: Propagating sort to sub-tables if True.
    """
    positions = map(lambda n, s=self: s.get_column_position(n), column_names)
    self.sort_positions(*positions, **params)
  
  def sort_positions(self, *positions, **params):
    """
    Sorting based on specified column positions.
    
    @todo: custom comparators.
    
    @param positions: column positions for ordering
    @param params: extra options
    
    @keyword reverse: boolean reversing ordering if True.
    @keyword deep: Propagating sort to sub-tables if True.
    """
    # propagates sorting to sub-tables
    if params.has_key('deep'):
      if params['deep']:
        for row_num in self._subtables.keys():
          t = self._subtables[row_num]
          t.sort_positions(*positions, **params)
    if params.has_key('reverse'):
      reverse = params['reverse']
    else:
      reverse = False
    # sorts current sub_tables
    tables = self._subtables.values()
    def ordering_func(t1, t2, positions=positions):
      cmp_result = 0
      for p in positions:
        if cmp_result == 0:
          cmp_result = cmp(t1.get_row_position(p), t2.get_row_position(p))
      return cmp_result
        
    tables.sort(ordering_func, reverse=reverse)
    self._subtables = dict(numbered(tables))
    self._sorting_positions = positions
    self._sorting_params = params
    
  def load(self, data):
    """
    @param data: array of objects to import
    @type data: list of list
    """
    self._subtables = dict()
    for row_num, row in numbered(data):
      t = self._create_subtable()
      self._subtables[row_num] = t
      for col_num, value in numbered(row):
        self._subtables[row_num].set_row_position(col_num, value)
    
  def set_value(self, row_num, column_num, value):
    if not self._subtables.has_key(row_num):
      t = self._create_subtable()
      self._subtables[row_num] = t
    self._subtables[row_num].set_row_position(column_num, value)
    
  def get_column_name(self, position):
    """
    Retrieving the name of a column using its name.
    
    @param position: position of column.
    @type position: int
    """
    if self.column_names.has_key(int(position)):
      return self.column_names[position]
    else:
      return str(position)
  
  def get_column_position(self, name):
    """
    Retrieving the name of a column using its position.
    
    @param name: name of column.
    @type name: string
    """
    if not self.column_names_positions.has_key(name):
      return int(name)
    return self.column_names_positions[name]
    
  def get_value(self, row_num, column_name):
    position = self.get_column_position(column_name)
    return self.get_position(row_num, position)
  
  def get_row_count(self):
    if self.is_leaf():
      count = 0
    else:
      count = max(self._subtables.keys()) + 1
    return count
    
  def get_column_count(self):
    if self.is_leaf():
      if len(self._aggregates.keys()) == 0:
        length = 0
      else :
        length = max(self._aggregates.keys()) + 1
    else:
      length = max(map(lambda r: r.get_column_count(), self._subtables.values()))
    return length
      
  def get_position(self, row_num, column_num):
    """
    Failsafe access to a cell.
    
    @param row_num: positive integer indicating the row to query
    @param column_num: positive integer indicating the column to query
    @return: the value of the cell or None if out of range or undefined
    """
    if self._subtables.has_key(row_num):
      value = self._subtables[row_num].get_row_position(column_num)
    else:
      value = None
    return value

  def extract_vector(self, agg_col):
    vector = []
    agg_pos = self.get_column_position(agg_col)
    for row_num in range(self.get_row_count()):
      vector.append(self.get_position(row_num, agg_pos))
    return vector
  
  def is_top(self):
    return self._parent_table is None
  
  def is_last_table(self):
    """ @return: True if the current table contains leaves
    """
    if self.get_row_count() > 0:
      return self._subtables[self._subtables.keys()[0]].is_leaf()
    else:
      return False
  
  def is_leaf(self):
    """ @return: True if the current table is a leaf
    """
    return len(self._subtables.keys()) == 0
    
  def set_level(self, level):
    self.level = level
    for row_num in self._subtables.keys():
      subtable = self._subtables[row_num]
      subtable.set_level(self.level + 1)
  
  def group(self, column_name, aggregates={}):
    group_position = self.get_column_position(column_name)
    return self.group_position(group_position, aggregates)
   
  def group_position(self, column_num, aggregators={}):
    """
    Grouping rows based on specified column and computing aggregates.
    """
    if self.is_last_table():
      groups = dict()
      for row_num in self._subtables.keys():
        subtable = self._subtables[row_num]
        # walking through each subtable aggregates
        group_value = subtable.get_row_position(column_num)
        if not groups.has_key(group_value):
          groups[group_value] = []
        groups[group_value].append(subtable)
      new_subtables = dict()
      for n, v in numbered(groups.keys()):
        # creates a new subtable for each common value
        group_table = self._create_subtable()
        # moves original tables to target group
        for m, t in numbered(groups[v]):
          group_table._attach_table(t, m)
        new_subtables[n] = group_table
        # computes aggregates of new table
        column_name = self.get_column_name(column_num)
        # sets default aggregate of new table based on column grouping
        parent_aggregators = self._aggregators
        new_aggregators = dict()
        for k in parent_aggregators.keys():
          new_aggregators[k] = parent_aggregators[k]
        for k in aggregators.keys():
          new_aggregators[k] = aggregators[k]          
        new_aggregators[column_name] = agg.agg_const(v)
        group_table._aggregators = new_aggregators
        group_table._create_aggregates()
      self._subtables = new_subtables
      self._update_aggregates()
    else:
      for row_num in self._subtables.keys():
        subtable = self._subtables[row_num]
        subtable.group_position(column_num, aggregators)
    self.sort_positions(*self._sorting_positions, **self._sorting_params)
    return self._subtables.values()
     
  def _update_aggregates(self):
    self._create_aggregates()
    if self._parent_table is not None:
      self._parent_table._update_aggregates()
    
  def _create_aggregates(self):
    for agg_col in self._aggregators.keys():
      vector = self.extract_vector(agg_col)
      agg_value = self._aggregators[agg_col](vector)
      agg_pos = self.get_column_position(agg_col)
      self.set_row_position(agg_pos, agg_value)

  def set_row_position(self, position, value):
    """    
    @param position: positive integer indicating the position we are setting
    """
    position = int(position)
    self._aggregates[position] = value
    
  def get_row_position(self, position):
    """
    Failsafe access to a cell.
    
    @param position: positive integer indicating the position to query
    @return: the value of the cell or None if out of range or undefined
    """
    position = int(position)
    if self._aggregates.has_key(position):
      return self._aggregates[position]
    else:
      return None
  
  def create_column(self, generator, name=None, position=None):
    """
    Creates a new column based on the specified column generator.
    
    @todo: redesign generator call
    
    """
    if position is None:
      position = self.get_column_count()
    if name == None:
      name = str(position)
    if self.is_leaf():
      # Looks very bad ...
      self._aggregates[position] = generator(self)
    else:
      for row_num in self._subtables.keys():
        t = self._subtables[row_num]
        t.create_column(generator, name, position)
  
  def get_table(self, row_num):
    """
    """
    if self._subtables.has_key(row_num):
      t = self._subtables[row_num]
    else:
      t = self._create_subtable()
    return t
  
  def __repr__(self):
    column_count = self.get_column_count()
    row_count = self.get_row_count()
    if self.is_leaf():
      indent_leaf = ' ' * (self.level - 2)
      out = indent_leaf + 'leaf:['
      for i in range(column_count):
        out += '%s; ' % str(self.get_row_position(i))
      out += ']' + os.linesep
    else:
      indent = ' ' * (self.level - 1)
      out = ''
      out += indent + 'aggregates:{'
      for i in range(column_count):
        out += '%s; ' % str(self.get_row_position(i))
      out += '}' + os.linesep
      for row_num in range(row_count):
        out += str(self.get_table(row_num))
    return out
  
